/*
 * Copyright (C) 2010 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.tracking.internal;

import java.io.*;
import java.util.*;
import ch.trackedbean.tracking.injection.*;
import ch.trackedbean.tracking.internal.injection.*;

/**
 * Abstract default implementation of a {@link IClassAnalyzer} looking for an direct implementation of a marker interface.
 * 
 * @author M. Hautle
 */
public abstract class AbstractClassAnalyzer implements IClassAnalyzer {
    /** General info entry for java.* classes. */
    private static final ClassInformation JAVA_CLASS = new ClassInformation();

    /** Entry for {@link Object}. */
    private static final ClassInformation OBJECT_CLASS = new ClassInformation("java/lang/Object", null, false, false, false);

    /** Map holding Informations about known classes */
    private final Map<String, ClassInformation> knownClasses = new HashMap<String, ClassInformation>();

    /** Stream to wrap a class byte array to analyze it with {@link ClassAnalyzer#analyze(InputStream)} (avoids double read). */
    private final ByteArrayStream classByteStream = new ByteArrayStream();

    /** The analyzer for class data. */
    private final ClassAnalyzer analyzer;

    /**
     * Default constructor.
     * 
     * @param marker The marker class name
     */
    public AbstractClassAnalyzer(String marker) {
        analyzer = new ClassAnalyzer(marker);
        knownClasses.put(OBJECT_CLASS.internalName, OBJECT_CLASS);
    }

    /**
     * {@link #getInformation(String)} implementation avoiding an re-read of the requested class.
     * 
     * @param name The internal class name
     * @param content The class content
     * @return The class information
     * @throws IOException If something went wrong
     */
    public ClassInformation getInformation(String name, byte[] content) throws IOException {
        // already known?
        final ClassInformation known = knownClasses.get(name);
        if (known != null)
            return known;
        // performance optimization by analyzing the passed byte array instead of rereading the class
        final ClassInformation ci = analyzer.analyze(classByteStream.setContent(content));
        final ClassInformation parent = getInformation(ci.parentName);
        ci.parentTracked = parent.tracked | parent.parentTracked;
        ci.serializable |= parent.serializable;
        knownClasses.put(ci.internalName, ci);
        return ci;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ClassInformation getInformation(String name) {
        if (name.startsWith("java/"))
            return JAVA_CLASS;
        // already known?
        ClassInformation known = knownClasses.get(name);
        if (known != null)
            return known;
        String parent = name;
        // all classes of the class structure
        final ArrayList<ClassInformation> structure = new ArrayList<ClassInformation>(2);
        // traverse the class structure
        while (parent != null) {
            ClassInformation ci = knownClasses.get(parent);
            if (ci == null)
                // if this is a java class we stop here by placing object as parent
                ci = parent.startsWith("java/") ? OBJECT_CLASS : resolveInformation(parent);
            structure.add(ci);
            parent = ci.parentName;
        }
        // the root of the class structure
        final ClassInformation root = structure.get(structure.size() - 1);
        knownClasses.put(root.internalName, root);
        // set the parentTracked and serializable flags
        boolean parentTracked = root.tracked;
        boolean parentSerializable = root.serializable;
        for (int i = structure.size() - 2; i > -1; i--) {
            final ClassInformation ci = structure.get(i);
            ci.parentTracked = parentTracked;
            ci.serializable |= parentSerializable;
            parentTracked |= ci.tracked;
            parentSerializable |= ci.serializable;
            knownClasses.put(ci.internalName, ci);
        }
        // the class you've asked for is the first entry in the list
        return structure.get(0);
    }

    /**
     * Resolves the {@link ClassInformation} for the given class.
     * 
     * @param clazz The internal class name. The internal name of a class is its fully qualified name (as returned by Class.getName()), where '.' are replaced
     *            by '/'
     * @return The {@link ClassInformation}
     */
    private ClassInformation resolveInformation(String clazz) {
        try {
            final InputStream in = getResourceStream(clazz.concat(".class"));
            if (in != null)
                return analyzer.analyze(in);
            System.err.println("Class not found: " + clazz);
        } catch (Exception e) {
            System.err.println("Error during class analyzing of " + clazz + " " + e.getMessage());
        }
        return OBJECT_CLASS;
    }

    /**
     * Returns the input stream for the specified resource.
     * 
     * @param name The resource name
     * @return The input stream for the resource or null if not found
     * @throws Exception If a (technical) error occurred
     */
    protected abstract InputStream getResourceStream(String name) throws Exception;

    /**
     * {@link ByteArrayInputStream} with exchangeable byte array.
     * 
     * @author M. Hautle
     */
    private static class ByteArrayStream extends ByteArrayInputStream {
        /** Empty byte array. */
        private static final byte[] EMPTY = new byte[0];

        /**
         * Default constructor.
         */
        public ByteArrayStream() {
            super(EMPTY);
        }

        /**
         * Sets the given byte array a stream content.
         * 
         * @param buf The content to set
         * @return The stream itself
         */
        ByteArrayStream setContent(byte[] buf) {
            this.buf = buf;
            this.pos = 0;
            this.count = buf.length;
            return this;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void close() throws IOException {
            setContent(EMPTY);
        }
    }
}
